调试是程序开发的重要的功能组成部分,可以用来发现程序出存在的问题,快速定位及解决,调试也可以辅助更好的理解程序。事实上,调试最初就是为了解决问题而产生的,调试的英文为debug, 而bug就是程序中存在的问题,debug就是解决掉这些问题。
1.调试的功能:
在调试中,程序的每一步的执行均是可控的,可以通过单步执行,设置断点等,控制程序的运行节奏,并在每次暂停时,都可以查看当前有效变量的具体值。
2.对初学者的意义:
调试中可以很明确的看到程序的执行过程,以及每一步产生的变化,对于初学者,这样直观的体验自然要比读代码更容易理解,而看到现象之后,再回想理论也更容易。
3.对编程者的意义:
当程序的运行结果与期望不符时,可以通过调试,查看每步的具体执行及结果,因此可以定位出到底是哪个操作或语句与预期的不同,从而快速定位,再针对性分析代码,实现快速解决问题的目的。
NSLog 使用
在XCode做开发调试时往往需要打印一些调试信息做debug用
NSLog 性能问题
它的运行会占用时间和设备资源。当打印信息的地方多了之后在模拟器上跑可能不会有什么问题,因为模拟器用的是电脑的硬件,但是当应用跑在设备上时这些输出语句会在很大程度上影响应用的性能,而且输出的数据也可能会暴露出App里的保密数据,所以发布正式版时需要把这些输出全部屏蔽掉,针对这种问题可以写一些宏来控制这些调试信息的输出。
解决方案:
简单粗暴的解决方案:在APP release前,将所有的NSLog注释掉,简单有效,但副作用是:下次你要调试时,又得将NSLog一个个取消注释。
正确的解决方案:你以release模式编译的程序不会用NSLog输出,而你以debug模式编译的程序将执行NSLog的全部功能。 在release版本禁止输出NSLog内容。
如何实现?
- 在 xxx.pch (预编译文件) 中添加以下代码:
1 | //用宏指令做一个判断,如果DEBUG为真,则编译#ifdef到#endif宏定义,否则编译器就不编译; |
- 设置DEBUG
在 “Target > Build Settings > Preprocessor Macros > Debug” 里有一个”DEBUG=1”。
设置为Debug模式下,点击Product –> Scheme –> Edit Scheme
设置run –> info –> Build Configuration成Debug时,就可以打印nslog了。
设置Release,发布app版本的时候就不会打印了,提高了性能。
如何自定义输入
在 xxx.pch 中添加以下代码:
1 | #ifdef DEBUG |
可以根据个人需要调整上面代码,主要就是这几个宏 ##__VA_ARGS , \FILE , \LINE 和\FUNCTION__。
1 | 1) __VA_ARGS__ 是一个可变参数的宏,很少人知道这个宏,这个可变参数的宏是新的C99规范中新增的,目前似乎只有gcc支持(VC6.0的编译器不支持)。宏前面加上##的作用在于,当可变参数的个数为0时,这里的##起到把前面多余的","去掉的作用,否则会编译出错, 你可以试试。 |
将Log日志重定向输出到文件
对于真机,日志没法保存,不好分析问题。所以有必要将日志保存到应用的Docunment目录下,并设置成共享文件,这样才能取出分析。
首先是日志输出,分为c的printf和标准的NSLog输出,printf会向标准输出(sedout)打印,而NSLog则是向标准出错(stderr),我们需要同时让他们都将日志打印到一个文件中。
1 | //例子: |
具体做法:
1 | // 将NSlog打印信息保存到Document目录下的文件中 |
当连接Mac调试的时候把这些注释掉,否则log只会输入到文件中,而不能从xcode的监视器中看到。
最后配置共享文件夹:
在应用程序的Info.plist文件中添加UIFileSharingEnabled键(Application supports iTunes file sharing 键),并将键值设置为YES。将您希望共享的文件放在应用程序的 Documents目录。一旦设备插入到用户计算机,iTunes 9.1就会在选中设备的Apps标签中找到自己的应用,查看共享内容,找到 log.txt 文件。
断言NSAssert()的使用
NSAssert()只是一个宏,用于开发阶段调试程序中的Bug,通过为NSAssert()传递条件表达式来断定是否属于Bug,满足条件返回真值,程序继续运行,如果返回假值,则抛出异常,并切可以自定义异常描述。NSAssert()是这样定义的:
1 | #define NSAssert(condition, desc) |
condition是条件表达式,值为YES或NO;desc为异常描述,通常为NSString。当conditon为YES时程序继续运行,为NO时,则抛出带有desc描述的异常信息。NSAssert()可以出现在程序的任何一个位置。具体事例如下:
生成一个LotteryEntry对象时,传入的NSDate不能为nil,加入NSAssert()判断。对象初始化源码如下:
1 | - (id)initWithEntryDate:(NSDate *)theDate { |
接下来则是生成对象时传入一个值为nil的NSDate,看断言是否运行。
LotteryEntry *nilEntry = [[LotteryEntry alloc] initWithEntryDate:nil];
断言效果如下:
1 | 2013-01-17 20:49:12.486 lottery[3951:303] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Argument must be non-nil' |
移除NSAssert比较简单,我们需要在target中选择build settings, 找到 preprocessor macros(预处理宏)项目,配置它的release为 NS_BLOCK_ASSERTIONS。
具体操作步骤为: 双击release的空白处,此时会弹出对话框,点击对话框中的+添加NS_BLOCK_ASSERTIONS。
Xcode 的LLDB 调试
先说一下Xcode怎样添加断点,以及调试区域在哪里,话不多说,请看图:
当代码走到断点处,会进入调试模式,在Xcode右下方的调试区域,有 (lldb) 显示。
常用 lldb 命令
- po 或 p 命令
调试器中最常用到的命令是 p (用于输出基本类型)或者 po (用于输出 Objective-C 对象),示例如下:
1 | // 调试命令 |
po 或 p 命令远没你想的那么简单,他还有执行代码的功能,比如:
1 | // 执行命令 |
- expression 或 expr 命令
常用于在调试过程中修改变量的值,而不用重新启动程序。
1 | // 执行命令 |
除了上面的用途,还可以新声明一个变量,比如:
1 | // 执行命令 |
- call 命令
call 命令和 po 命令的功能相似,有的地方说“在不需要显示输出,或是方法无返回值时使用call”,但是使用 po 命令系统会向编代码一样提示你方法名或变量名,但是 call 命令不会啊,没想出有什么理由放弃 po 而使用 call 。
- bt 命令
bt 命令用来打印主线程的堆栈信息,bt all 可以打印所有线程的堆栈信息。
- image 命令
image 命令可用于寻址,有多个组合命令。比较实用的用法是用于寻找栈地址对应的代码位置。
比如以下代码:
1 | NSArray *array = @[@"1",@"2"]; |
这段代码会报数组越界的错误,错误信息如下:
1 | *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayI objectAtIndex:]: index 3 beyond bounds [0 .. 1]' |
可能出错的地址是0x00000001072b51f6(可以根据执行文件名判断,或者最小的栈地址)。为了进一步精确定位,我们可以输入以下的命令:
1 | // 执行命令 |
- 简称和别名
有些调试命令比较长,使用起来比较麻烦,我们可以自定义别名。比如给image lookup –address 添加了一个 ila 的别名。
1 | // 执行命令 |
等等,还没有结束,还有一个非常重要的命令 help,如果你对expr 命令不了解,可以使用help expr 来查看更多关于expr 命令的信息。
调试技巧
请看图:
双击断点,选择编辑选项,开始编辑断点。
1 | Condition:此处是判断断点的执行条件,当(BOOL)[username isEqualToString:@"admin"]为 YES 时,执行断点。 |
总结
调试方法的灵活使用,可以减轻开发负担,同时也能更加准确的找到问题所在。这里只是平时常用的几个命令,想要更多的命令,还需要自己取学习。
参考网站 The LLDB Debugger